Worker
工作流概述
这是一个包含29个节点的复杂工作流,主要用于自动化处理各种任务。
工作流源代码
{
"id": 114,
"name": "Standup Bot - Worker",
"nodes": [
{
"name": "publish report",
"type": "n8n-nodes-base.mattermost",
"position": [
1840,
1040
],
"parameters": {
"message": "={{$node[\"Prep Report\"].json[\"post\"]}}",
"channelId": "={{$node[\"Prep Report\"].json[\"channel\"]}}",
"attachments": [],
"otherOptions": {}
},
"credentials": {
"mattermostApi": {
"id": "2",
"name": "Mattermost account"
}
},
"typeVersion": 1
},
{
"name": "get user data",
"type": "n8n-nodes-base.httpRequest",
"position": [
1400,
1040
],
"parameters": {
"url": "={{$node[\"Read Config 2\"].json[\"config\"][\"mattermostBaseUrl\"]}}/api/v4/users/{{$node[\"Action from MM\"].json[\"body\"][\"user_id\"]}}",
"options": {},
"jsonParameters": true,
"headerParametersJson": "={
\"Authorization\": \"Bearer {{$item(0).$node[\"Read Config 2\"].json[\"config\"][\"botUserToken\"]}}\"
}"
},
"typeVersion": 1
},
{
"name": "open-standup-dialog?",
"type": "n8n-nodes-base.if",
"position": [
1180,
1260
],
"parameters": {
"conditions": {
"string": [
{
"value1": "={{$node[\"Action from MM\"].json[\"body\"][\"context\"][\"action\"]}}",
"value2": "open-standup-dialog"
}
]
}
},
"typeVersion": 1
},
{
"name": "Action from MM",
"type": "n8n-nodes-base.webhook",
"position": [
520,
820
],
"webhookId": "6a28d86b-9f74-4825-9785-57e0d43b198f",
"parameters": {
"path": "standup-bot/action/f6f9b174745fa4651f750c36957d674c",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 1
},
{
"name": "Slash Cmd from MM",
"type": "n8n-nodes-base.webhook",
"position": [
520,
600
],
"webhookId": "72732516-1143-430f-8465-d193fe657311",
"parameters": {
"path": "standup-bot/slashCmd",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 1
},
{
"name": "config?",
"type": "n8n-nodes-base.if",
"position": [
740,
600
],
"parameters": {
"conditions": {
"string": [
{
"value1": "={{$node[\"Slash Cmd from MM\"].json[\"body\"][\"text\"]}}",
"value2": "config"
}
]
}
},
"typeVersion": 1
},
{
"name": "open config dialog",
"type": "n8n-nodes-base.httpRequest",
"position": [
1360,
580
],
"parameters": {
"url": "={{$node[\"Read Config 1\"].json[\"config\"][\"mattermostBaseUrl\"]}}/api/v4/actions/dialogs/open",
"options": {
"bodyContentType": "json"
},
"requestMethod": "POST",
"jsonParameters": true,
"bodyParametersJson": "={{$json}}"
},
"typeVersion": 1
},
{
"name": "Prep Config Dialog",
"type": "n8n-nodes-base.function",
"position": [
1160,
580
],
"parameters": {
"functionCode": "const channelId =
$item(0).$node['Slash Cmd from MM'].json['body']['channel_id'];
const configuredStandups =
$item(0).$node['Read Config 1'].json['standups'] ?? [];
let standup = configuredStandups.find(
(standup) => standup.channelId == channelId
);
// define default values:
if (!standup) {
standup = {
title: 'Team Standup',
time: '09:00',
days: [1, 2, 3, 4, 5],
questions: [
'What have you accomplished since your last report?',
'What do you want to accomplish until your next report?',
'Is anything blocking your progress?',
],
users: [],
};
}
const payload = {
trigger_id: $item(0).$node['Slash Cmd from MM'].json['body']['trigger_id'],
url: $item(0).$node['Read Config 1'].json['config']['n8nWebhookUrl'],
dialog: {
callback_id: 'standup-config',
title: 'Standup Configuration',
submit_label: 'Save',
notify_on_cancel: false,
state: JSON.stringify({ standupId: channelId }),
elements: [
{
display_name: 'Standup title',
name: 'title',
type: 'text',
placeholder: 'Team Standup',
default: standup.title,
optional: true,
help_text:
'💡 The standup can be deleted by setting its title to an empty string!',
},
{
display_name: 'Time',
name: 'time',
type: 'select',
default: standup.time,
options: [
{
text: '06:00',
value: '06:00',
},
{
text: '07:00',
value: '07:00',
},
{
text: '08:00',
value: '08:00',
},
{
text: '09:00',
value: '09:00',
},
{
text: '10:00',
value: '10:00',
},
{
text: '11:00',
value: '11:00',
},
{
text: '12:00',
value: '12:00',
},
{
text: '13:00',
value: '13:00',
},
{
text: '14:00',
value: '14:00',
},
{
text: '15:00',
value: '15:00',
},
{
text: '16:00',
value: '16:00',
},
{
text: '17:00',
value: '17:00',
},
],
},
{
display_name: 'Days',
name: 'days',
type: 'text',
placeholder: '1,2,3,4,5',
help_text:
'comma-separated; 0=Sun | 1=Mon | 2=Tue | 3=Wed | 4=Thu | 5=Fri | 6=Sat',
default: standup.days.join(','),
},
{
display_name: 'Questions',
name: 'questions',
type: 'textarea',
help_text: 'Max 5 questions, one question per line;',
default: standup.questions.join('\n'),
},
{
display_name: 'Users',
name: 'users',
type: 'textarea',
help_text: 'One user per line',
default: standup.users.join('\n'),
},
],
},
};
return [{ json: payload }];
"
},
"typeVersion": 1
},
{
"name": "callback ID?",
"type": "n8n-nodes-base.switch",
"position": [
960,
820
],
"parameters": {
"rules": {
"rules": [
{
"value2": "standup-config"
},
{
"output": 1,
"value2": "standup-answers"
}
]
},
"value1": "={{$node[\"Action from MM\"].json[\"body\"][\"callback_id\"]}}",
"dataType": "string",
"fallbackOutput": 3
},
"typeVersion": 1
},
{
"name": "standup-config",
"type": "n8n-nodes-base.noOp",
"position": [
1180,
820
],
"parameters": {},
"typeVersion": 1
},
{
"name": "standup-answers",
"type": "n8n-nodes-base.noOp",
"position": [
1180,
1040
],
"parameters": {},
"typeVersion": 1
},
{
"name": "Prep Config Override",
"type": "n8n-nodes-base.function",
"position": [
1400,
820
],
"parameters": {
"functionCode": "const mattermostInput = $item(0).$node['Action from MM'].json['body'];
const config = $item(0).$node['Read Config 2'].json;
// ensure there is a \"standups\" array:
config['standups'] = config['standups'] ?? [];
// remove the standup from the list:
config['standups'] = config['standups'].filter(
(standup) => standup.channelId != mattermostInput.channel_id
);
const textToArray = (text, separator) => {
return text
.split(separator)
.map((e) => e.trim())
.filter((e) => e.length > 0);
};
// a standup can be deleted by updating its title to \"\"
if (mattermostInput.submission.title.length > 0) {
const newStandup = {
channelId: mattermostInput.channel_id,
title: mattermostInput.submission.title,
time: mattermostInput.submission.time,
days: textToArray(mattermostInput.submission.days, ',').map((e) =>
parseInt(e)
),
users: textToArray(mattermostInput.submission.users, '\n'),
questions: textToArray(mattermostInput.submission.questions, '\n'),
};
config['standups'].push(newStandup);
}
return [{ json: config }];
"
},
"typeVersion": 1
},
{
"name": "Override Config",
"type": "n8n-nodes-base.executeWorkflow",
"position": [
1620,
820
],
"parameters": {
"workflowId": "1005"
},
"typeVersion": 1
},
{
"name": "Read Config 1",
"type": "n8n-nodes-base.executeWorkflow",
"position": [
960,
580
],
"parameters": {
"workflowId": "1004"
},
"typeVersion": 1
},
{
"name": "Read Config 2",
"type": "n8n-nodes-base.executeWorkflow",
"position": [
740,
820
],
"parameters": {
"workflowId": "1004"
},
"typeVersion": 1
},
{
"name": "confirm success",
"type": "n8n-nodes-base.mattermost",
"position": [
1840,
820
],
"parameters": {
"userId": "={{$node[\"Action from MM\"].json[\"body\"][\"user_id\"]}}",
"message": "new standup config was saved successfully",
"channelId": "={{$node[\"Action from MM\"].json[\"body\"][\"channel_id\"]}}",
"operation": "postEphemeral"
},
"credentials": {
"mattermostApi": {
"id": "2",
"name": "Mattermost account"
}
},
"typeVersion": 1
},
{
"name": "Read Config 3",
"type": "n8n-nodes-base.executeWorkflow",
"position": [
740,
380
],
"parameters": {
"workflowId": "1004"
},
"typeVersion": 1
},
{
"name": "Filter Due Standups",
"type": "n8n-nodes-base.function",
"position": [
960,
380
],
"parameters": {
"functionCode": "const config = $item(0).$node['Read Config 3'].json;
// ensure there is a \"standups\" array:
config['standups'] = config['standups'] ?? [];
const now = new Date();
const duePattern = `${now.getDay()}_${now
.getHours()
.toString()
.padStart(2, '0')}:00`; // e.g. 1_13:00 => Monday 1 p.m.
console.log(duePattern);
// filter standups that are due now:
const dueStandups = config.standups.filter((standup) =>
//true
standup.days.map((day) => `${day}_${standup.time}`).includes(duePattern)
);
return dueStandups.map((standup) => ({
json: standup,
}));
"
},
"typeVersion": 1
},
{
"name": "Prep Request Standup",
"type": "n8n-nodes-base.function",
"position": [
1180,
380
],
"parameters": {
"functionCode": "const reminders = items.reduce((prev, curr) => {
return prev.concat(
curr.json.users.map((user) => ({
channelId: curr.json.channelId,
title: curr.json.title,
user: user,
}))
);
}, []);
return reminders.map((reminder) => ({
json: reminder,
}));
"
},
"typeVersion": 1
},
{
"name": "Create Channel",
"type": "n8n-nodes-base.httpRequest",
"position": [
1620,
380
],
"parameters": {
"url": "={{$item(0).$node[\"Read Config 3\"].json[\"config\"][\"mattermostBaseUrl\"]}}/api/v4/channels/direct",
"options": {},
"requestMethod": "POST",
"jsonParameters": true,
"bodyParametersJson": "=[\"{{$node[\"Get User\"].json[\"id\"]}}\", \"{{$item(0).$node[\"Read Config 3\"].json[\"config\"][\"botUserId\"]}}\"]",
"headerParametersJson": "={
\"Authorization\": \"Bearer {{$item(0).$node[\"Read Config 3\"].json[\"config\"][\"botUserToken\"]}}\"
}"
},
"typeVersion": 1
},
{
"name": "Remind Users",
"type": "n8n-nodes-base.httpRequest",
"position": [
2060,
380
],
"parameters": {
"url": "={{$item(0).$node[\"Read Config 3\"].json[\"config\"][\"mattermostBaseUrl\"]}}/api/v4/posts",
"options": {},
"requestMethod": "POST",
"jsonParameters": true,
"bodyParametersJson": "={{$json}}",
"headerParametersJson": "={
\"Authorization\": \"Bearer {{$item(0).$node[\"Read Config 3\"].json[\"config\"][\"botUserToken\"]}}\"
}"
},
"typeVersion": 1
},
{
"name": "Get User",
"type": "n8n-nodes-base.httpRequest",
"position": [
1400,
380
],
"parameters": {
"url": "={{$item(0).$node[\"Read Config 3\"].json[\"config\"][\"mattermostBaseUrl\"]}}/api/v4/users/username/{{$node[\"Prep Request Standup\"].json[\"user\"]}}",
"options": {},
"jsonParameters": true,
"headerParametersJson": "={
\"Authorization\": \"Bearer {{$item(0).$node[\"Read Config 3\"].json[\"config\"][\"botUserToken\"]}}\"
}"
},
"typeVersion": 1,
"continueOnFail": true
},
{
"name": "Prep Reminder",
"type": "n8n-nodes-base.function",
"position": [
1840,
380
],
"parameters": {
"functionCode": "const webhookUrl =
$item(0).$node['Read Config 3'].json['config']['n8nWebhookUrl']; // e.g. https://xyz.app.n8n.cloud/webhook-test/standup-bot/action/top-secret-api-key
const botUserToken =
$item(0).$node['Read Config 3'].json['config']['botUserToken'];
let itemIndex = 0;
for (item of items) {
const directChannelId = item.json.id;
const payload = {
channel_id: directChannelId,
props: {
attachments: [
{
pretext: \"Hi there! It's time for standup!\",
text: `Please provide your input for: **${
$item(itemIndex).$node['Prep Request Standup'].json['title']
}**`,
actions: [
{
id: webhookUrl.includes('test') ? 'webhook-test' : 'webhook',
name: 'Provide Update',
integration: {
url: webhookUrl,
context: {
action: 'open-standup-dialog',
secret: botUserToken, // not ideal but good enough for now...
standupId:
$item(itemIndex).$node['Prep Request Standup'].json[
'channelId'
],
},
},
},
],
},
],
},
};
item.json = payload;
itemIndex++;
}
return items;
"
},
"typeVersion": 1
},
{
"name": "Prep Standup Dialog",
"type": "n8n-nodes-base.function",
"position": [
1400,
1240
],
"parameters": {
"functionCode": "const standupId =
$item(0).$node['Action from MM'].json['body']['context']['standupId'];
const postId = $item(0).$node['Action from MM'].json['body']['post_id'];
const configuredStandups =
$item(0).$node['Read Config 2'].json['standups'] ?? [];
let standup = configuredStandups.find(
(standup) => (standup.channelId == standupId)
);
const renderQuestions = (questions) => {
let questionId = 1;
return questions.map((question) => ({
display_name: question,
name: `q${questionId++}`,
type: 'textarea',
}));
};
const payload = {
trigger_id: $item(0).$node['Action from MM'].json['body']['trigger_id'],
url: $item(0).$node['Read Config 2'].json['config']['n8nWebhookUrl'],
dialog: {
callback_id: 'standup-answers',
title: `Report for: ${standup.title}`,
submit_label: 'Submit',
notify_on_cancel: false,
state: JSON.stringify({ standupId, reminderPostId: postId }),
elements: renderQuestions(standup.questions),
},
};
return [{ json: payload }];
"
},
"typeVersion": 1
},
{
"name": "open standup dialog",
"type": "n8n-nodes-base.httpRequest",
"position": [
1600,
1240
],
"parameters": {
"url": "={{$node[\"Read Config 2\"].json[\"config\"][\"mattermostBaseUrl\"]}}/api/v4/actions/dialogs/open",
"options": {
"bodyContentType": "json"
},
"requestMethod": "POST",
"jsonParameters": true,
"bodyParametersJson": "={{$json}}"
},
"typeVersion": 1
},
{
"name": "Prep Report",
"type": "n8n-nodes-base.function",
"position": [
1620,
1040
],
"parameters": {
"functionCode": "const { standupId, reminderPostId } = JSON.parse(
$item(0).$node['Action from MM'].json['body']['state']
);
const submission = $item(0).$node['Action from MM'].json['body']['submission'];
const configuredStandups = $item(0).$node['Read Config 2'].json['standups'];
const standup = configuredStandups.find(
(standup) => standup.channelId == standupId
);
const emptyAnswers = [
'-',
'/',
' ',
'x',
'n/a',
'nope',
'nopes',
'no',
'none',
'no.',
'nothing',
];
function capitalize(text) {
return text.charAt(0).toUpperCase() + text.slice(1);
}
const renderPost = (submission, standup) => {
let postText = `### ${capitalize(
$item(0).$node['get user data'].json['username']
)}\n`;
let questionIndex = 0;
postText += standup.questions
.map((question) => {
questionIndex++;
if (
!submission[`q${questionIndex}`] ||
emptyAnswers.includes(submission[`q${questionIndex}`].toLowerCase())
) {
return '';
}
return `#### ${question}\n${submission[`q${questionIndex}`]}`;
})
.join('\n');
return postText;
};
return [
{
json: {
post: renderPost(submission, standup),
channel: standupId,
reminderPostId,
standupTitle: standup.title,
},
},
];
"
},
"typeVersion": 1
},
{
"name": "Delete ReminderPost",
"type": "n8n-nodes-base.mattermost",
"position": [
2280,
1040
],
"parameters": {
"postId": "={{$node[\"Prep Report\"].json[\"reminderPostId\"]}}",
"operation": "delete"
},
"credentials": {
"mattermostApi": {
"id": "2",
"name": "Mattermost account"
}
},
"typeVersion": 1
},
{
"name": "Update Post",
"type": "n8n-nodes-base.httpRequest",
"position": [
2060,
1040
],
"parameters": {
"url": "={{$node[\"Read Config 2\"].json[\"config\"][\"mattermostBaseUrl\"]}}/api/v4/posts/{{$node[\"Prep Report\"].json[\"reminderPostId\"]}}",
"options": {},
"requestMethod": "PUT",
"jsonParameters": true,
"bodyParametersJson": "={
\"id\":\"{{$node[\"Prep Report\"].json[\"reminderPostId\"]}}\",
\"message\": \"Thank you for providing your report for {{$node[\"Prep Report\"].json[\"standupTitle\"]}}\"
}",
"headerParametersJson": "={
\"Content-Type\":\"application/json\",
\"Authorization\": \"Bearer {{$item(0).$node[\"Read Config 2\"].json[\"config\"][\"botUserToken\"]}}\"
}"
},
"typeVersion": 1
},
{
"name": "Every hour",
"type": "n8n-nodes-base.cron",
"position": [
520,
380
],
"parameters": {
"triggerTimes": {
"item": [
{
"mode": "custom",
"cronExpression": "0 0 6-12 * * 1-5"
}
]
}
},
"typeVersion": 1
}
],
"active": false,
"settings": {},
"connections": {
"config?": {
"main": [
[
{
"node": "Read Config 1",
"type": "main",
"index": 0
}
]
]
},
"Get User": {
"main": [
[
{
"node": "Create Channel",
"type": "main",
"index": 0
}
]
]
},
"Every hour": {
"main": [
[
{
"node": "Read Config 3",
"type": "main",
"index": 0
}
]
]
},
"Prep Report": {
"main": [
[
{
"node": "publish report",
"type": "main",
"index": 0
}
]
]
},
"callback ID?": {
"main": [
[
{
"node": "standup-config",
"type": "main",
"index": 0
}
],
[
{
"node": "standup-answers",
"type": "main",
"index": 0
}
],
[],
[
{
"node": "open-standup-dialog?",
"type": "main",
"index": 0
}
]
]
},
"Prep Reminder": {
"main": [
[
{
"node": "Remind Users",
"type": "main",
"index": 0
}
]
]
},
"Read Config 1": {
"main": [
[
{
"node": "Prep Config Dialog",
"type": "main",
"index": 0
}
]
]
},
"Read Config 2": {
"main": [
[
{
"node": "callback ID?",
"type": "main",
"index": 0
}
]
]
},
"Read Config 3": {
"main": [
[
{
"node": "Filter Due Standups",
"type": "main",
"index": 0
}
]
]
},
"get user data": {
"main": [
[
{
"node": "Prep Report",
"type": "main",
"index": 0
}
]
]
},
"Action from MM": {
"main": [
[
{
"node": "Read Config 2",
"type": "main",
"index": 0
}
]
]
},
"Create Channel": {
"main": [
[
{
"node": "Prep Reminder",
"type": "main",
"index": 0
}
]
]
},
"publish report": {
"main": [
[
{
"node": "Update Post",
"type": "main",
"index": 0
}
]
]
},
"standup-config": {
"main": [
[
{
"node": "Prep Config Override",
"type": "main",
"index": 0
}
]
]
},
"Override Config": {
"main": [
[
{
"node": "confirm success",
"type": "main",
"index": 0
}
]
]
},
"standup-answers": {
"main": [
[
{
"node": "get user data",
"type": "main",
"index": 0
}
]
]
},
"Slash Cmd from MM": {
"main": [
[
{
"node": "config?",
"type": "main",
"index": 0
}
]
]
},
"Prep Config Dialog": {
"main": [
[
{
"node": "open config dialog",
"type": "main",
"index": 0
}
]
]
},
"Filter Due Standups": {
"main": [
[
{
"node": "Prep Request Standup",
"type": "main",
"index": 0
}
]
]
},
"Prep Standup Dialog": {
"main": [
[
{
"node": "open standup dialog",
"type": "main",
"index": 0
}
]
]
},
"Prep Config Override": {
"main": [
[
{
"node": "Override Config",
"type": "main",
"index": 0
}
]
]
},
"Prep Request Standup": {
"main": [
[
{
"node": "Get User",
"type": "main",
"index": 0
}
]
]
},
"open-standup-dialog?": {
"main": [
[
{
"node": "Prep Standup Dialog",
"type": "main",
"index": 0
}
]
]
}
}
}
功能特点
- 自动检测新邮件
- AI智能内容分析
- 自定义分类规则
- 批量处理能力
- 详细的处理日志
技术分析
节点类型及作用
- Mattermost
- Httprequest
- If
- Webhook
- Function
复杂度评估
配置难度:
维护难度:
扩展性:
实施指南
前置条件
- 有效的Gmail账户
- n8n平台访问权限
- Google API凭证
- AI分类服务订阅
配置步骤
- 在n8n中导入工作流JSON文件
- 配置Gmail节点的认证信息
- 设置AI分类器的API密钥
- 自定义分类规则和标签映射
- 测试工作流执行
- 配置定时触发器(可选)
关键参数
| 参数名称 | 默认值 | 说明 |
|---|---|---|
| maxEmails | 50 | 单次处理的最大邮件数量 |
| confidenceThreshold | 0.8 | 分类置信度阈值 |
| autoLabel | true | 是否自动添加标签 |
最佳实践
优化建议
- 定期更新AI分类模型以提高准确性
- 根据邮件量调整处理批次大小
- 设置合理的分类置信度阈值
- 定期清理过期的分类规则
安全注意事项
- 妥善保管API密钥和认证信息
- 限制工作流的访问权限
- 定期审查处理日志
- 启用双因素认证保护Gmail账户
性能优化
- 使用增量处理减少重复工作
- 缓存频繁访问的数据
- 并行处理多个邮件分类任务
- 监控系统资源使用情况
故障排除
常见问题
邮件未被正确分类
检查AI分类器的置信度阈值设置,适当降低阈值或更新训练数据。
Gmail认证失败
确认Google API凭证有效且具有正确的权限范围,重新进行OAuth授权。
调试技巧
- 启用详细日志记录查看每个步骤的执行情况
- 使用测试邮件验证分类逻辑
- 检查网络连接和API服务状态
- 逐步执行工作流定位问题节点
错误处理
工作流包含以下错误处理机制:
- 网络超时自动重试(最多3次)
- API错误记录和告警
- 处理失败邮件的隔离机制
- 异常情况下的回滚操作